import util from 'node:util'
import http from 'node:http'
import Stream from "node:stream"
import statuses from '../http/statuses'
import httpAssert from '../http/httpAssert'
import createError from '../http/httpErrors'
import Cookies from '../http/Cookies'
import { HttpRequest } from './HttpRequest'
import { HttpResponse } from './HttpResponse'
import { IHttpApplication, IHttpContext, IHttpRequest, IHttpResponse } from "../interface"

const COOKIES = Symbol('context#cookies')

export default class HttpContext implements IHttpContext {
  state: object = {}
  request: IHttpRequest
  response: IHttpResponse

  /**
   * @constructor
   * @param {HttpApplication} app
   * @param {IncomingMessage} req
   * @param {ServerResponse} res
   */
  constructor(
    public app: IHttpApplication,
    public req: http.IncomingMessage,
    public res: http.ServerResponse) {
    this.state = {}
    this.request = new HttpRequest(app, this, req, res)
    this.response = new HttpResponse(app, this, req, res)
  }

  /**
   * just returns the JSON output.（util.inspect）
   */
  inspect(): any {
    return this.toJSON()
  }

  /**
   * Return JSON representation.
   *
   * Here we explicitly invoke .toJSON() on each
   * object, as iteration will otherwise fail due
   * to the getters and cause utilities such as
   * clone() to fail.
   */
  toJSON(): any {
    return {
      app: this.app.toJSON(),
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      url: this.req.url,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    }
  }

  /**
   * Similar to .throw(), adds assertion.
   *
   *    this.assert(this.user, 401, 'Please login!');
   *
   * See: https://github.com/jshttp/http-assert
   */
  assert = httpAssert

  /**
   * Throw an error with `status` (default 500) and
   * `msg`. Note that these are user-level
   * errors, and the message may be exposed to the client.
   *
   *    this.throw(403)
   *    this.throw(400, 'name required')
   *    this.throw('something exploded')
   *    this.throw(new Error('invalid'))
   *    this.throw(400, new Error('invalid'))
   *
   * See: httpErrors
   *
   * Note: `status` should only be passed as the first parameter.
   *
   * @param {String|Number|Error} err, msg or status
   * @param {String|Number|Error} [err, msg or status]
   * @param {Object} [props]
   */
  throw(...args: any[]): void {
    throw createError(...args)
  }

  /**
   * Default error handling.
   */
  onerror(err: Error | any) {
    // don't do anything if there is no error.
    // this allows you to pass `this.onerror`
    // to node-style callbacks.
    if (err == null) return

    // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
    // See https://github.com/koajs/koa/issues/1466
    // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err))

    let headerSent = false
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true
    }

    // delegate
    this.app.emit('error', err, this)

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return
    }

    const { res } = this

    // first unset all headers
    res.getHeaderNames().forEach(name => res.removeHeader(name))

    // then set those specified
    this.set(err.headers)

    // force text/plain
    this.type = 'text'

    let statusCode = err.status || err.statusCode

    // ENOENT support
    if (err.code === 'ENOENT') statusCode = 404

    // default to 500
    if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500

    // respond
    const code = statuses[statusCode]
    const msg = err.expose ? err.message : code
    this.status = err.status = statusCode
    this.length = Buffer.byteLength(msg)
    res.end(msg)
  }

  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      })
    }
    return this[COOKIES]
  }

  set cookies(_cookies) {
    this[COOKIES] = _cookies
  }

  // https://blog.csdn.net/qdmoment/article/details/103014631
  /******************************************************************
   * Request delegation.
   * ****************************************************************/
  acceptsLanguages(...args: any[]): any[] | string {
    return this.request.acceptsLanguages(...args)
  }

  acceptsEncodings(...args: any[]): string | any[] {
    return this.request.acceptsEncodings(...args)
  }

  acceptsCharsets(...args: any[]): string | any[] {
    return this.request.acceptsCharsets(...args)
  }

  accepts(...args: any[]): string | any[] | false {
    return this.request.accepts(...args)
  }

  get(field: string): string {
    return this.request.get(field)
  }

  is(type?: string | string[], ...types: string[]): string | false | null {
    return this.request.is(type, ...types)
  }

  get headers(): http.IncomingHttpHeaders {
    return this.request.headers
  }

  set headers(val: http.IncomingHttpHeaders) {
    this.request.headers = val
  }

  get method(): string {
    return this.request.method
  }

  set method(val: string) {
    this.request.method = val
  }

  get protocol(): string {
    return this.request.protocol
  }

  get host(): string {
    return this.request.host
  }

  get hostname(): string {
    return this.request.hostname
  }

  get origin(): string {
    return this.request.origin
  }

  get search(): string {
    return this.request.search
  }

  get searchParams(): URLSearchParams {
    return this.request.searchParams
  }

  get pathname(): string {
    return this.request.pathname
  }

  get url(): string {
    return this.request.url
  }

  get href(): string {
    return this.request.href
  }

  get URL(): URL | any {
    return this.request.URL
  }

  get accept(): any {
    return this.request.accept
  }

  set accept(obj: any) {
    this.request.accept = obj
  }

  get subdomains(): string[] {
    return this.request.subdomains
  }

  get secure(): boolean {
    return this.request.secure
  }

  get stale(): boolean {
    return this.request.stale
  }

  get fresh(): boolean {
    return this.request.fresh
  }

  get ips(): string[] {
    return this.request.ips
  }

  get ip(): string {
    return this.request.ip
  }

  /******************************************************************
   * Response delegation.
   * ****************************************************************/
  attachment(filename: string, options: any) {
    return this.response.attachment(filename, options)
  }

  redirect(url: string, alt: string): void {
    this.response.redirect(url, alt)
  }

  remove(field: string): void {
    return this.response.remove(field)
  }

  vary(field: string): void {
    return this.response.vary(field)
  }

  has(field: string): boolean {
    return this.response.has(field)
  }

  set(field: string, val: string | Array<string>): void
  set(field: object | Array<any>): void
  set(field: any, val: any = undefined): void {
    return this.response.set(field, val)
  }

  append(field: string, val: string | string[]): void {
    return this.response.append(field, val)
  }

  flushHeaders(): void {
    return this.response.flushHeaders()
  }

  get status(): number {
    return this.response.status
  }

  set status(code: number) {
    this.response.status = code
  }

  get message(): string {
    return this.response.message
  }

  set message(msg: string) {
    this.response.message = msg
  }

  get body(): string | Buffer | object | Stream {
    return this.response.body
  }

  set body(val: string | Buffer | object | Stream) {
    this.response.body = val
  }

  get length(): number {
    return this.response.length
  }

  set length(n: number) {
    this.response.length = n
  }

  get type(): string {
    return this.response.type
  }

  set type(type: string) {
    this.response.type = type
  }

  get lastModified(): Date {
    return this.response.lastModified
  }

  set lastModified(val: string | Date) {
    this.response.lastModified = val
  }

  get etag(): string {
    return this.response.etag
  }

  set etag(val: string) {
    this.response.etag = val
  }

  get headerSent(): boolean {
    return this.response.headerSent
  }

  get writable(): boolean {
    return this.response.writable
  }
}
